package com.moyun.syssecurity.config;


import com.moyun.syssecurity.filter.JwtRequestFilter;
import com.moyun.syssecurity.handler.*;
import com.moyun.service.system.MyUserDetailsService;
import com.moyun.syssecurity.sms.config.SmsCodeAuthenticationSecurityConfig;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.access.AccessDecisionVoter;
import org.springframework.security.access.vote.RoleVoter;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import org.springframework.security.web.session.HttpSessionEventPublisher;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import javax.sql.DataSource;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)// 有这个注解，@PreAuthorize 才能有效
@RequiredArgsConstructor
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 依赖说明
     * 自定义登录(认证)成功处理器--->MyAuthenticationSuccessHandler
     * 自定义登录(认证)失败处理器--->MyAuthenticationFailureHandler
     * 自定义权限异常处理器--->MyAccessDeniedHandler
     * 自定义登出成功处理器--->MyLogoutSuccessHandler
     * 自定义的密码加密器--->MyPasswordEncoder
     * 自定义JwtToke认证入口--->JwtAuthenticationEntryPoint
     * 自定义UserDetailsService接口实现--->MyUserDetailsService
     * JdbcTokenRepositoryImpl依赖的DataSource--->DataSource
     * 手机短信验证码登录配置类--->SmsCodeAuthenticationSecurityConfig
     * 动态url权限配置类--->MySecurityMetadataSource(FilterInvocationSecurityMetadataSource 接口的实现类)
     * httpSession会话创建与销毁事件订阅--->HttpSessionEventPublisher
     **/

    private final MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
    private final MyAuthenticationFailureHandler myAuthenticationFailureHandler;
    private final MyAccessDeniedHandler myAccessDeniedHandler;
    private final MyLogoutSuccessHandler myLogoutSuccessHandler;
    private final DataSource dataSource;
    private final MyPasswordEncoder passwordEncoder;
    private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
    private final MyUserDetailsService userDetailsService;
    private final SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfig;
    private final JwtRequestFilter jwtRequestFilter;
    private final MySecurityMetadataSource mySecurityMetadataSource;
//    private final SpringSessionBackedSessionRegistry<Session> redisSessionRegistry;


    /**
     * 为AuthenticationManager配置userDetailService与PasswordEncoder
     */
    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth
                // 用户认证处理
                .userDetailsService(userDetailsService)
                // 密码处理
                .passwordEncoder(passwordEncoder);
    }


    /**
     * @return void
     * @Author yangboxuan
     * @Description 配置静态资源过滤 忽略拦截url或静态资源文件夹 - web.ignoring(): 会直接过滤该url - 将不会经过Spring Security过滤器链
     * @Date 2021/5/24 11:38
     * @Param [web]
     **/
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring()
                .antMatchers(STATIC_RESOURCE)
                .antMatchers(DRUID_RESOURCE)
                .antMatchers(SWAGGER_RESOURCE);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        /**
         * 添加短信验证码配置类
         */
        http.apply(smsCodeAuthenticationSecurityConfig);

        /**
         * 将图片验证码过滤器添加在 UsernamePasswordAuthenticationFilter 过滤器之前
         */
//        http.addFilterBefore(new VerificationCodeFilter(), UsernamePasswordAuthenticationFilter.class);

        /**
         * 后台请求拦截配置
         */
        http
                .authorizeRequests()
                // /xxurl 无需认证和授权即可访问(即匿名访问)
                .antMatchers("/webSocket/**").permitAll()
                .antMatchers("/captcha.jpg").permitAll()
                .antMatchers("/sms/get-code").permitAll()
                // 认证后才能访问系统接口
                .anyRequest().authenticated()
                // 匿名访问系统中的所有接口
//                .anyRequest().permitAll()
                .and();

        /**
         * 添加动态url权限配置 (此配置默认数据库中没有记录的uri,均可以匿名访问)
         */
        // 方案一 :  此配置默认数据库中没有记录的uri,均可以匿名访问(即对外暴露系统中的所有接口), 后台请求拦截配置失效
        /*ApplicationContext applicationContext = http.getSharedObject(ApplicationContext.class);
        http.apply(new UrlAuthorizationConfigurer<>(applicationContext))
                .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
                    @Override
                    public <O extends FilterSecurityInterceptor> O postProcess(O o) {
                        o.setSecurityMetadataSource(mySecurityMetadataSource);
                        return o;
                    }
                });*/

        // 方案二 :  对外暴露系统中的所有接口, 后台请求拦截配置有效
        http
                //定制url访问权限，动态权限读取，参考：https://www.jianshu.com/p/0a06496e75ea
                .addFilterAfter(dynamicallyUrlInterceptor(), FilterSecurityInterceptor.class);
        /**
         * 表单登录相关配置
         */
        http
                .formLogin()
                .loginPage("/login.html")
                // 指定处理登录请求的路径
                .loginProcessingUrl("/login")
                // 应用AuthenticationDetailsSource
//                .authenticationDetailsSource(myWebAuthenticationDetailsSource)
//                // 指定表单登录使用的用户账号参数和密码参数 如果不配置，默认是username和password
//                .usernameParameter("name")
//                .passwordParameter("passwd")
                .successHandler(myAuthenticationSuccessHandler)
                .failureHandler(myAuthenticationFailureHandler)
                .permitAll()
                .and();
        /**
         * 系统退出相关配置
         */
        http
                // 系统退出相关配置
                .logout()
                // 通过 logoutUrl 方法来修改默认的注销 URL。  默认的注销URL就是logout Url
                .logoutUrl("/logout")
//                .logoutRequestMatcher(new AntPathRequestMatcher("/logout","POST"))  // 方法不仅可以修改注销 URL，还可以修改请求方式，实际项目中，这个方法和 logoutUrl 任意设置一个即可。
                //.logoutSuccessUrl("/index")  // 表示注销成功后要跳转的页面
                .addLogoutHandler(new MyLogoutHandler())
                //登出成功的处理程序
                .logoutSuccessHandler(myLogoutSuccessHandler)
                // 用来清除 cookie 分别表示清除认证信息和使 HttpSession 失效，默认可以不用配置，默认就会清除
//                .deleteCookies()
//                .clearAuthentication(true)
//                .invalidateHttpSession(true)
                .permitAll()
                .and();
        /**
         * 开启记住我功能
         */
//        http
//                .rememberMe()
//                .userDetailsService(userDetailsService)
//                .tokenRepository(persistentTokenRepository())
//                // 永远打开记住我状态
//                .alwaysRemember(true)
//                .and();
        /**
         * 添加自定义过滤器
         */
        http
//                .addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class)
                .addFilterBefore(jwtRequestFilter, LogoutFilter.class)
                // 凭证无效时的处理 进入jwtToken认证入口
                .exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint)
                .and();
        /**
         * 启用Spring Security的CORS支持
         */
//        http
//                .cors()
//                .and();
        /**
         * 跨站请求伪造防护配置
         */
        http
                .csrf()
                .disable()
                // 不使用cookie-session认证，此策略使得每次请求都要自行处理权限问题（往SecurityContextHolder.context中添加和查询Authentication）
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        /**
         * Session 会话并发配置
         */
        http
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        // Session达到指定时间,失效后的策略(操作)
//                .invalidSessionStrategy(new MyInvalidSessionStrategy())
        // 并发Session 会话 上限为1
//                .maximumSessions(1)
//                // 方案一: 使用spring-security 提供的会话注册表(存在于系统应用内存中)
//                .sessionRegistry(sessionRegistry())
//                // 方案二: 使用spring-session 提供的会话注册表(存在于Redis缓存中)
////                .sessionRegistry(redisSessionRegistry)
//                // 达到上限后是否阻止登录  maxSessionsPreventsLogin True代表将阻止后续的登录操作
//                .maxSessionsPreventsLogin(false)
//                // 失效Session 策略 上步.maxSessionsPreventsLogin(false) 下面的策略才会生效
//                .expiredSessionStrategy(new MyParallelismSessionExpiredStrategy());
        /**
         * 自定义异常配置 (url路径拦截会进入此配置) 方法拦截(如@PreAuthorize("hasRole('xx')")) 会进入全局异常处理器GlobalExceptionHandler
         */
        http
                .exceptionHandling()
                .accessDeniedHandler(myAccessDeniedHandler);


    }


    /**
     * @return log.springframework.security.access.hierarchicalroles.RoleHierarchy
     * @Author yangboxuan
     * @Description 角色继承 功能实现:所有 user 能够访问的资源，admin 都能够访问
     * @Date 2021/5/24 11:50
     * @Param []
     **/
//    @Bean
//    RoleHierarchy roleHierarchy() {
//        RoleHierarchyImpl hierarchy = new RoleHierarchyImpl();
//        hierarchy.setHierarchy("ROLE_admin > ROLE_user");
//        return hierarchy;
//    }

    /**
     * @return log.springframework.web.cors.CorsConfigurationSource
     * @Author yangboxuan
     * @Description 跨站请求伪造配置
     * @Date 2021/5/24 11:39
     * @Param []
     **/
    @Bean
    CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        // 允许从百度站点跨域
        corsConfiguration.setAllowedOrigins(Arrays.asList("https://www.baidu.com"));
        // 允许使用GET方法和POST方法
        corsConfiguration.setAllowedMethods(Arrays.asList("GET", "POST"));
        // 允许带凭证
        corsConfiguration.setAllowCredentials(true);
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        // 对所有URL生效
        source.registerCorsConfiguration("/**", corsConfiguration);
        return source;
    }

    /**
     * @return org.springframework.security.authentication.AuthenticationManager
     * @Author yangboxuan
     * @Description 将AuthenticationManager交给spring管理
     * @Date 2021/5/30 20:02
     * @Param []
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    // -- swagger ui忽略 swagger静态资源直接放行
    private final String[] SWAGGER_RESOURCE = {
            // -- swagger ui
            "/v2/api-docs",
            "/swagger-resources/**",
            "/swagger-ui.html",
            "/webjars/**",
            // swagger-boostrap-ui
            "/doc.html"
    };


    // -- 系统静态资源直接放行
    private final String[] STATIC_RESOURCE = {
            "/",
            "/js/**",
            "/css/**",
            "/img/**",
            "/fonts/**",
            "/static/css/**",
            "/logging.html",
            "/favicon.ico",
            "/templates/**",
            "chatroom.html"
    };

    // -- Druid的静态资源直接放行
    private final String[] DRUID_RESOURCE = {
            "/druid/**",

    };

    /**
     * @return com.moyun.security.config.DynamicallyUrlInterceptor
     * @Author yangboxuan
     * @Description 配置filter 用于实现动态url权限  (系统中的所有接口,用户认证后(登陆后) 才能访问)
     * @Date 2021/7/5 14:09
     * @Param []
     **/
//    @Bean
    public DynamicallyUrlInterceptor dynamicallyUrlInterceptor() {
        //首次获取
        mySecurityMetadataSource.updateURIRoleMap();
        //初始化拦截器并添加数据源
        DynamicallyUrlInterceptor interceptor = new DynamicallyUrlInterceptor();
        interceptor.setSecurityMetadataSource(mySecurityMetadataSource);

        //配置RoleVoter决策
        List<AccessDecisionVoter<? extends Object>> decisionVoters = new ArrayList<>();
        decisionVoters.add(new RoleVoter());

        //设置认证决策管理器
        interceptor.setAccessDecisionManager(new MyAccessDecisionManager(decisionVoters));
        return interceptor;
    }

    /**
     * @return org.springframework.security.web.authentication.rememberme.PersistentTokenRepository
     * @Author yangboxuan
     * @Description 创建PersistentTokenRepository 持久令牌存储层 交由Spring管理
     * @Date 2021/7/4 16:55
     * @Param []
     */
    @Bean
    public PersistentTokenRepository persistentTokenRepository() {
        JdbcTokenRepositoryImpl persistentTokenRepository = new JdbcTokenRepositoryImpl();
        persistentTokenRepository.setDataSource(dataSource);
        return persistentTokenRepository;
    }

    /**
     * @return org.springframework.security.core.session.SessionRegistry
     * @Author yangboxuan
     * @Description 创建SessionRegistry Session会话注册表 交由Spring管理
     * 此时的 sessionRegistry 会话注册表是放在系统应用缓存中的,跟随系统应用的启动关闭来创建和销毁
     * @Date 2021/7/4 16:54
     * @Param []
     */
    @Bean
    public SessionRegistry sessionRegistry() {
        // 用于访问Session
        return new SessionRegistryImpl();
    }

    /**
     * @return org.springframework.security.web.session.HttpSessionEventPublisher
     * @Author yangboxuan
     * @Description httpSession 事件订阅(session会话的创建与销毁)
     * 如果系统开启了会话管理,必须向Spring容器中注入此类
     * @Date 2021/8/30 10:49
     * @Param []
     **/
    @Bean()
    public HttpSessionEventPublisher httpSessionEventPublisher() {
        return new HttpSessionEventPublisher();
    }


}
